package cn.lysoy.demo.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * 单节点redis锁
 * @author SongYang
 */
@Service
public class RedisLock {
    private final RedisTemplate<String, String> redisTemplate;

    /**存储锁值**/
    private static final ThreadLocal<String> local = new ThreadLocal<>();

    @Autowired
    public RedisLock(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 加锁
     *  加锁时，每个节点产生一个随机字符串    ----->    避免锁误删
     *  写入随机数与设置失效时间必须是同时    ----->    保证加锁的原子性
     * @param key
     * @param value 一般用UUID,如果value相同，则认为是同一个线程的请求，则认为重入锁
     * @param ttl    必须给锁设置一个失效时间            ----->    避免死锁
     * @return  true 加锁成功
     * @author SongYang
     */
    public boolean lock(final String key, String value, long ttl) {
        List<String> keys = new ArrayList<>(2);
        keys.add(key);
        keys.add(value);
        String luaScript = buildSetLockLuaScript();
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        try {
            Long flag = redisTemplate.execute(redisScript, keys, String.valueOf(ttl));
            if (1 == flag) {
                return true;
            }
            // 如果设置延迟时间失败，为避免死锁，即时释放锁
        }catch (RedisSystemException rse) {
            rse.printStackTrace();
            unlock(key, value);
        }
        return false;
    }

    /**
     * 释放锁
     */
    public boolean unlock(final String key, String value) {
        List<String> keys = new ArrayList<>(2);
        keys.add(key);
        keys.add(value);
        String luaScript = buildUnLockLuaScript();
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        Long flag = redisTemplate.execute(redisScript, keys);
        if (1 == flag) {return true;}
        return false;
    }

    /**
     * 加线程锁
     */
    public boolean lock(final String key, long ttl) {
        String uuid = UUID.randomUUID().toString();
        boolean lock = lock(key, uuid, ttl);
        if (lock) {
            local.set(uuid);
            return true;
        }
        return false;
    }

    /**
     * 释放线程锁
     */
    public boolean unlock(final String key) {
        String value = local.get();
        boolean unlock = unlock(key, value);
        if (unlock) {
            local.remove();
            return true;
        }
        return false;
    }

    /**
     * 加锁脚本,通过setnx 向特定的key写入一个随机数，并设置失效时间，写入成功即加锁成功
     * @return lua脚本
     */
    private String buildSetLockLuaScript() {
        return
                "local key = KEYS[1]"+
                        "\nlocal content = KEYS[2]"+
                        "\nlocal ttl     = ARGV[1]"+
                        "\nlocal lockSet = redis.call('setnx', key, content)"+
                        "\nif lockSet == 1 then"+
                        "\nredis.call('pexpire', key, ttl)"+
                        "\nelse"+
                        "\nlocal value = redis.call('get', key)"+
                        "\nif(value == content) then"+
                        "\nlockSet = 1;"+
                        "\nredis.call('pexpire', key, ttl)"+
                        "\nend"+
                        "\nend"+
                        "\nreturn lockSet";
    }

    /**
     * 解锁脚本
     * @return lua脚本
     */
    private String buildUnLockLuaScript() {
        return
                "local key = KEYS[1]"+
                        "\nlocal content = KEYS[2]"+
                        "\nlocal value = redis.call('get', key)"+
                        "\nif value == content then"+
                        "\nreturn redis.call('del', key);"+
                        "\nend"+
                        "\nreturn 0";
    }
}
